# recommender.py
import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import MultiLabelBinarizer
from collections import defaultdict
import logging

# 初始化日志
logger = logging.getLogger(__name__)


class TourismRecommender:
    def __init__(self, data_path="cleaned_tourism_data.csv"):
        # 初始化数据
        self.df = None
        self.user_item_matrix = None
        self.user_features = None
        self.global_site_popularity = None
        self.mlb = MultiLabelBinarizer()
        self.scaler = MinMaxScaler()
        self.site_columns = []

        # 加载数据
        self._load_data(data_path)
        self._prepare_features()
        self._create_global_popularity()

        logger.info("Recommender system initialized successfully")

    def _load_data(self, data_path):
        """加载并预处理原始数据"""
        try:
            self.df = pd.read_csv(data_path)

            # 清洗景点名称
            self.df['Sites Visited'] = self.df['Sites Visited'].str.findall(r"'([^']*)'")
            self.df['Sites Visited'] = self.df['Sites Visited'].apply(
                lambda x: [s.strip(' []"\'') for s in x]
            )

            # 处理评分
            self.df['Tourist Rating'] = pd.to_numeric(
                self.df['Tourist Rating'], errors='coerce'
            ).fillna(self.df['Tourist Rating'].mean())

            self.df['Satisfaction'] = pd.to_numeric(
                self.df['Satisfaction'], errors='coerce'
            ).fillna(self.df['Satisfaction'].mean())

            # 计算综合评分
            self.df['Composite_Score'] = (
                                                 0.5 * (self.df['Tourist Rating'] - self.df['Tourist Rating'].min()) /
                                                 (self.df['Tourist Rating'].max() - self.df['Tourist Rating'].min()) +
                                                 0.5 * (self.df['Satisfaction'] - self.df['Satisfaction'].min()) /
                                                 (self.df['Satisfaction'].max() - self.df['Satisfaction'].min())
                                         ) * 5

        except Exception as e:
            logger.error(f"Data loading failed: {str(e)}")
            raise

    def _prepare_features(self):
        """准备特征矩阵"""
        # 创建用户-景点矩阵
        exploded_df = self.df.explode('Sites Visited')
        self.user_item_matrix = exploded_df.pivot_table(
            index='Tourist ID',
            columns='Sites Visited',
            values='Composite_Score',
            aggfunc='mean',
            fill_value=0
        )
        self.site_columns = self.user_item_matrix.columns.tolist()

        # 处理用户特征
        user_features = self.df.groupby('Tourist ID').agg({
            'Age': 'first',
            'Interests': 'first',
            'Preferred Tour Duration': 'first'
        }).reset_index()

        # 清洗兴趣特征
        def clean_interests(interest_str):
            if pd.isna(interest_str):
                return []
            return [s.strip().lower() for s in interest_str.split(',')]

        user_features['Interests'] = user_features['Interests'].apply(clean_interests)

        # 多热编码
        interests_encoded = pd.DataFrame(
            self.mlb.fit_transform(user_features['Interests']),
            columns=[s.lower().strip() for s in self.mlb.classes_],
            index=user_features.index
        )

        # 归一化数值特征
        user_features[['Age_Norm', 'Duration_Norm']] = self.scaler.fit_transform(
            user_features[['Age', 'Preferred Tour Duration']]
        )

        # 合并特征
        self.user_features = pd.concat([
            user_features[['Tourist ID', 'Age_Norm', 'Duration_Norm']],
            interests_encoded
        ], axis=1).set_index('Tourist ID')

    def _create_global_popularity(self):
        """创建全局热门景点数据"""
        exploded_df = self.df.explode('Sites Visited')
        self.global_site_popularity = exploded_df.groupby('Sites Visited').agg(
            avg_score=('Composite_Score', 'mean'),
            visit_count=('Composite_Score', 'count')
        )
        self.global_site_popularity['popularity'] = (
                self.global_site_popularity['avg_score'] * 0.7 +
                self.global_site_popularity['visit_count'] * 0.3
        )
        self.global_site_popularity = self.global_site_popularity['popularity'].sort_values(ascending=False)

    def _prepare_new_user(self, user_info):
        """准备新用户特征向量"""
        # 处理兴趣
        interests = [s.strip().lower() for s in user_info['interests'].split(',') if s.strip()]

        # 特征编码
        try:
            interests_encoded = self.mlb.transform([interests])
        except ValueError:
            interests_encoded = np.zeros((1, len(self.mlb.classes_)))

        interests_df = pd.DataFrame(
            interests_encoded,
            columns=self.mlb.classes_,
            index=[0]
        )

        # 归一化数值特征
        age_duration = self.scaler.transform([[
            user_info['age'],
            user_info['preferred_tour_duration']
        ]])

        # 构建特征字典
        features = {
            'Age_Norm': age_duration[0][0],
            'Duration_Norm': age_duration[0][1]
        }
        features.update(interests_df.iloc[0].to_dict())

        # 添加访问历史
        for site in self.site_columns:
            features[site] = 1 if site in user_info['sites_visited'] else 0

        return pd.DataFrame([features]).reindex(columns=self.user_features.columns, fill_value=0)

    def get_recommendations(self, user_info, n=5, k=3):
        """
        生成推荐结果
        :param user_info: 包含用户信息的字典
        :param n: 返回推荐数量
        :param k: 相似用户数量
        :return: 推荐结果列表 (景点, 评分)
        """
        try:
            # 准备新用户数据
            new_user_df = self._prepare_new_user(user_info)

            # 计算相似度
            combined = pd.concat([self.user_features, new_user_df])
            similarity = cosine_similarity(combined)[-1, :-1]
            similarity_series = pd.Series(similarity, index=self.user_features.index)

            # 获取有效相似用户
            valid_users = similarity_series[similarity_series.index.isin(self.user_item_matrix.index)]
            similar_users = valid_users.nlargest(k)

            recommendations = []
            visited = set(user_info['sites_visited'])

            # 协同过滤推荐
            if not similar_users.empty:
                for site in self.user_item_matrix.columns:
                    if site not in visited:
                        try:
                            ratings = self.user_item_matrix.loc[similar_users.index, site]
                            weighted_sum = (similar_users.values * ratings).sum()
                            sum_weights = similar_users.sum()
                            score = weighted_sum / sum_weights if sum_weights != 0 else 0
                            recommendations.append((site, score))
                        except KeyError:
                            continue

                # 取前N个推荐
                recommendations = sorted(recommendations, key=lambda x: x[1], reverse=True)[:n]

            # 混合推荐逻辑
            if len(recommendations) < n:
                # 兴趣推荐
                interest_recs = []
                for interest in user_info['interests'].lower().split(','):
                    interest = interest.strip()
                    if interest in self.mlb.classes_:
                        site_scores = self.df.explode('Sites Visited')
                        site_scores = site_scores[site_scores['Interests'].apply(
                            lambda x: interest in [s.strip().lower() for s in x]
                        )]
                        interest_popularity = site_scores.groupby('Sites Visited')['Composite_Score'].mean()
                        interest_recs.extend(interest_popularity.sort_values(ascending=False).index.tolist())

                # 全局热门推荐
                global_recs = self.global_site_popularity.index.tolist()

                # 合并推荐
                all_recs = [
                    (site, self.global_site_popularity[site])
                    for site in interest_recs + global_recs
                    if site not in visited
                ]

                # 去重排序
                seen = set()
                unique_recs = []
                for site, score in all_recs:
                    if site not in seen:
                        seen.add(site)
                        unique_recs.append((site, score))

                # 补充推荐
                recommendations += sorted(unique_recs, key=lambda x: x[1], reverse=True)[:n - len(recommendations)]

            return recommendations[:n]

        except Exception as e:
            logger.error(f"Recommendation error: {str(e)}")
            return []